Skip to content

Enforce target novel length via expansion agent and length validation#164

Merged
CyberSecDef merged 10 commits into
mainfrom
copilot/enforce-target-novel-length
Apr 15, 2026
Merged

Enforce target novel length via expansion agent and length validation#164
CyberSecDef merged 10 commits into
mainfrom
copilot/enforce-target-novel-length

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 15, 2026

Generated chapters frequently fall short of target word counts with no validation or corrective mechanism. The pipeline accepted whatever the LLM produced regardless of length.

Changes

  • Config (novelforge/config.py): Add CHAPTER_MIN_LENGTH_PCT (default 85%, clamped to 50–100 via _get_percentage_env()) and MAX_EXPANSION_ATTEMPTS (default 2), both env-overridable
  • Expansion agent (prompts.yml, prompts.py): New chapter_expansion prompt template and build_chapter_expansion_prompt() builder that asks the LLM to deepen scenes/dialogue/interiority without altering plot
  • Length validation (_helpers.py): check_chapter_length() pure validation + expand_chapter() with retry logic and graceful failure
  • Pipeline integration (pipeline.py): _run_all_chapter_agents() accepts target_words; runs expansion via the _safe() wrapper (which injects vocabulary rules and content-retry) after all refinement passes, followed by a post-expansion vocabulary scan/fix-up to prevent reintroduction of forbidden words
  • Generation route (chapters.py): Passes target_per_chapter to pipeline, tracks length_enforcement_log and total_words_generated per-chapter, surfaces results in progress updates
  • Draft prompt (prompts.yml): Strengthened from soft guidance ("approximately X words") to mandatory language with structural requirements (3+ scenes, scene components); prompt wording aligned with the actual 85% minimum threshold enforcement
  • Progress schema (progress.py): Added length_enforcement field to ProgressState with documented schema: {chapter_num, target, min_threshold, actual, meets_min_threshold, total_words_so_far}

Key flow

# After all refinement agents complete:
actual_wc, min_threshold, acceptable = check_chapter_length(text, target_words, min_pct)
if not acceptable:
    # Expansion routes through _safe() for vocabulary-rule injection + content-retry
    for attempt in range(1, max_attempts + 1):
        expanded = _safe(build_expansion_prompt, text, action=...)
        if len(expanded.split()) > actual_wc:
            text = expanded
    # Post-expansion vocabulary scan to catch reintroduced forbidden words
    post_violations = scan_vocabulary_overuse(text, genre=genre)
    if post_violations:
        text = _safe(build_vocabulary_fix_prompt, text, action=...)

Expansion is best-effort: if the LLM call fails or doesn't increase length, the original text is kept. No change in behavior when target_words=0 (backward compatible).

Tests

24 new tests in test_length_enforcement.py covering check_chapter_length, expand_chapter retry/failure semantics, prompt builder, config constants, and schema.

Copilot AI requested review from Copilot and removed request for Copilot April 15, 2026 01:09
Copilot AI linked an issue Apr 15, 2026 that may be closed by this pull request
- Add CHAPTER_MIN_LENGTH_PCT and MAX_EXPANSION_ATTEMPTS config constants
- Add chapter_expansion prompt template in prompts.yml
- Add build_chapter_expansion_prompt() prompt builder
- Add check_chapter_length() and expand_chapter() to _helpers.py
- Wire expansion into pipeline via target_words parameter
- Add per-chapter length tracking with length_enforcement_log
- Strengthen chapter_draft prompt with mandatory length requirements
- Add length_enforcement field to ProgressState
- Add 24 new tests covering all length enforcement components

Agent-Logs-Url: https://github.com/CyberSecDef/NovelForge/sessions/edfa2372-2014-43cf-8520-a4f613600a1b

Co-authored-by: CyberSecDef <17597068+CyberSecDef@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 15, 2026 01:23
…xpansion prompt

Agent-Logs-Url: https://github.com/CyberSecDef/NovelForge/sessions/edfa2372-2014-43cf-8520-a4f613600a1b

Co-authored-by: CyberSecDef <17597068+CyberSecDef@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 15, 2026 01:26
Copilot AI changed the title [WIP] Enhance LLM generation pipeline to enforce target novel length Enforce target novel length via expansion agent and length validation Apr 15, 2026
Copilot AI requested a review from CyberSecDef April 15, 2026 01:27
@CyberSecDef CyberSecDef marked this pull request as ready for review April 15, 2026 01:30
Copilot AI review requested due to automatic review settings April 15, 2026 01:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a “chapter length enforcement” mechanism to the chapter-generation pipeline so chapters that are too short can be automatically expanded and the results surfaced via progress metadata.

Changes:

  • Introduces chapter-length validation and a best-effort expansion retry loop.
  • Integrates length enforcement into the chapter refinement pipeline and generation progress updates.
  • Adds config knobs + prompt templates/builders + a dedicated test suite for the feature.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/test_length_enforcement.py New unit tests for length checking, expansion behavior, prompt builder, config exports, and schema field presence.
prompts.yml Tightens chapter draft length language and adds a new chapter_expansion prompt template.
novelforge/routes/generation/chapters.py Passes per-chapter target words into the pipeline and records per-chapter length enforcement info in progress updates.
novelforge/progress.py Extends ProgressState schema with length_enforcement.
novelforge/config.py Adds CHAPTER_MIN_LENGTH_PCT and MAX_EXPANSION_ATTEMPTS configuration values and exports them.
novelforge/agents/chapter/prompts.py Adds build_chapter_expansion_prompt() that renders the new expansion prompt.
novelforge/agents/chapter/pipeline.py Runs expansion after refinement passes (post copy-edit) when a chapter is under the minimum threshold.
novelforge/agents/chapter/_helpers.py Adds check_chapter_length() and expand_chapter() helpers.
novelforge/agents/chapter/__init__.py Re-exports new helpers and prompt builder.
config.py Re-exports the new config constants via the backward-compatibility shim.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread novelforge/config.py Outdated
Comment thread prompts.yml Outdated
Comment thread novelforge/agents/chapter/pipeline.py Outdated
Comment on lines +472 to +493
# Length enforcement — expand under-length chapters after all refinement
# passes have finished so that the expansion doesn't get trimmed.
if target_words > 0:
from novelforge.agents.chapter._helpers import check_chapter_length, expand_chapter
import novelforge.config as _cfg

min_pct = _cfg.CHAPTER_MIN_LENGTH_PCT
actual_wc, min_threshold, acceptable = check_chapter_length(text, target_words, min_pct)
if not acceptable:
_check_deadline()
if step_callback:
step_callback(
f"Chapter {chapter_num}: expanding ({actual_wc} words, need {min_threshold})"
)
text, actual_wc = expand_chapter(
text,
target_words=target_words,
min_words=min_threshold,
chapter_num=chapter_num,
title=title,
max_attempts=_cfg.MAX_EXPANSION_ATTEMPTS,
)
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Length enforcement runs after the vocabulary scan/fix-up step, but expand_chapter() calls the LLM without vocabulary-rule injection and no subsequent scan occurs. This can reintroduce forbidden/soft-limited terms after the last vocabulary enforcement pass. Consider either (a) running the vocabulary scan/fix-up again after expansion, or (b) performing expansion via the existing _safe() wrapper (so vocab rules + content-retry apply) instead of calling call_llm directly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 8f03266. The expansion pass now routes through the _safe() wrapper (which injects vocabulary rules and applies content-retry), and a post-expansion vocabulary scan + fix-up pass runs afterwards to catch any forbidden/overused words reintroduced by the expansion LLM call.

Comment thread novelforge/routes/generation/chapters.py
Comment thread novelforge/progress.py Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 15, 2026 01:41
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread novelforge/config.py Outdated
Comment thread novelforge/progress.py Outdated
Comment thread tests/test_length_enforcement.py Outdated
Comment on lines +262 to +268
def test_chapter_min_length_pct_default(self):
import novelforge.config as cfg
assert cfg.CHAPTER_MIN_LENGTH_PCT == 85

def test_max_expansion_attempts_default(self):
import novelforge.config as cfg
assert cfg.MAX_EXPANSION_ATTEMPTS == 2
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests assert the default values of CHAPTER_MIN_LENGTH_PCT/MAX_EXPANSION_ATTEMPTS, but both settings are env-overridable and computed at import time. If either env var is set in the test environment, these assertions will fail. Make the tests deterministic by clearing the env vars with monkeypatch and reloading novelforge.config (e.g., importlib.reload) before asserting defaults.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in a0e6d6f. Both test_chapter_min_length_pct_default and test_max_expansion_attempts_default now use monkeypatch.delenv() to clear the env var and importlib.reload() to re-compute the module-level constant, with a try/finally to restore the module afterwards.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 15, 2026 01:45
CyberSecDef and others added 2 commits April 14, 2026 21:46
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…re-run vocab scan post-expansion, align prompt with 85% threshold, rename meets_target to meets_min_threshold, fix schema comment

Agent-Logs-Url: https://github.com/CyberSecDef/NovelForge/sessions/134a2fbe-cf62-4dda-8cc9-b0834d96eeeb

Co-authored-by: CyberSecDef <17597068+CyberSecDef@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +420 to +428
def expand_chapter(
text: str,
*,
target_words: int,
min_words: int,
chapter_num: int,
title: str,
max_attempts: int = 2,
) -> tuple[str, int]:
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expand_chapter() accepts a title parameter but never uses it (not in logging, not in the prompt builder). Either remove the parameter to avoid dead API surface, or incorporate title into the expansion prompt/log context so callers aren’t passing unused data.

Copilot uses AI. Check for mistakes.
Comment on lines +500 to +514
# Track length enforcement results
from novelforge.agents.chapter._helpers import check_chapter_length
min_pct = config.CHAPTER_MIN_LENGTH_PCT
_, min_threshold, meets_min = check_chapter_length(
text, target_per_chapter, min_pct,
)
total_words_generated += chapter_word_count
length_enforcement_log.append({
"chapter_num": chapter_num,
"target": target_per_chapter,
"min_threshold": min_threshold,
"actual": chapter_word_count,
"meets_min_threshold": meets_min,
"total_words_so_far": total_words_generated,
})
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The boolean stored as meets_target comes from check_chapter_length(), which checks actual >= min_threshold (derived from CHAPTER_MIN_LENGTH_PCT), not whether actual >= target_per_chapter. This name is misleading for consumers of the progress metadata; consider renaming the field to something like meets_min_threshold/acceptable, or compute a separate meets_target against the true target.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 15, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • api.openai.com
    • Triggering command: /usr/bin/python python -m pytest tests/ -q --tb=short (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI and others added 2 commits April 15, 2026 01:54
Copilot AI requested review from Copilot and removed request for Copilot April 15, 2026 01:55
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 15, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • api.openai.com
    • Triggering command: /usr/bin/python python -m pytest tests/ -x --tb=short (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@CyberSecDef CyberSecDef merged commit 836808b into main Apr 15, 2026
7 checks passed
@CyberSecDef CyberSecDef deleted the copilot/enforce-target-novel-length branch April 15, 2026 01:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce Target Novel Length in LLM Generation Pipeline

3 participants